%%%-------------------------------------------------------------------
%% @doc cache worker process.
%% @end
%%%-------------------------------------------------------------------

-module(cache_worker).

-behaviour(gen_server).

-include_lib("eunit/include/eunit.hrl").


-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([load/2, save/3, delete/2]).

-record(state, {module, save_time, gc_time}).

-callback start() -> Reason when
    Reason     :: term().

-callback load(Key) -> Data when
    Key        :: term(),
    Data       :: term().

-callback save(Data) -> ok when
    Data       :: term().

-callback delete(Key) -> ok when
    Key        :: term().

-callback db_load(Key) -> Data when
    Key        :: term(),
    Data       :: term().

-callback db_save(Data) -> ok when
    Data       :: term().

-callback db_delete(Key) -> ok when
    Key        :: term().


%%%-------------------------------------------------------------------
load(Module, all) ->
    [Data || {_Key, Data} <- ets:tab2list(Module)];
load(Module, Key) ->
    case ets:lookup(Module, Key) of
        [] ->
            gen_server:call(Module, {load, Key});
        [{Key, Data}] ->
            Data
    end.

save(Module, Key, Data) ->
    gen_server:cast(Module, {save, Key, Data}),
    ok.

delete(Module, Key) ->
    gen_server:cast(Module, {delete, Key}),
    ok.

%%%-------------------------------------------------------------------
start_link(Args) ->
    Module = proplists:get_value(module, Args),
    gen_server:start_link({local, Module}, ?MODULE, Args, []).

init(Args) ->
    process_flag(trap_exit, true),
    Module   = proplists:get_value(module,      Args),
    SaveTime = proplists:get_value(save_time,   Args, 0),
    GCTime   = proplists:get_value(gc_time,     Args, 604800),  %% 7 day
    ReadAhead= proplists:get_value(readahead,   Args, false),   %% 数据预读
    ets:new(Module, [{keypos, 1}, named_table, public, set, {read_concurrency, true}, {write_concurrency, true}]),
    send_after(SaveTime, save_time),
    send_after(GCTime, gc_time),
    gen_server:cast(self(), {readahead, ReadAhead}),
    State = #state{module = Module, save_time = SaveTime, gc_time = GCTime},
    {ok, State}.

handle_call(Request, From, State) ->
    try
        do_call(Request, From, State)
    catch
        Type:Reason:Stack ->
            ?debugFmt("handle_call error:~n"
                      " -- Request:~p, From:~p, State:~p~n"
                      " -- Type:~p, Reason:~p~n"
                      " -- stacktrace:~p",
                      [Request, From, State, Type, Reason, Stack]),
            {reply, fail, State}
    end.

handle_cast(Msg, State) ->
    try
        do_cast(Msg, State)
    catch
        Type:Reason:Stack ->
            ?debugFmt("handle_cast error:~n"
                      " -- Msg:~p, State:~p~n"
                      " -- Type:~p, Reason:~p~n"
                      " -- stacktrace:~p",
                      [Msg, State, Type, Reason, Stack]),
            {noreply, State}
    end.
    

handle_info(Info, State) ->
    try
        do_info(Info, State)
    catch
        Type:Reason:Stack ->
            ?debugFmt("handle_info error:~n"
                      " -- Info:~p, State:~p~n"
                      " -- Type:~p, Reason:~p~n"
                      " -- stacktrace:~p",
                      [Info, State, Type, Reason, Stack]),
            {noreply, State}
    end.

terminate(_Reason, State) ->
    %% 保存所有数据
    Module = State#state.module,
    Module:db_save([D || {_K, D, _T} <- get_save_list()]),
    set_save_list([]),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


%% internal functions
%%%-------------------------------------------------------------------
%% 手动落地所有脏数据
do_call(save, _From, State) ->
    Module = State#state.module,
    Module:db_save([D || {_K, D, _T} <- get_save_list()]),
    set_save_list([]),
    {reply, ok, State};

do_call({load, Key}, _From, State) ->
    Module = State#state.module,
    Data = Module:db_load(Key),
    ets:insert(Module, {Key, Data}),
    {reply, Data, State};

do_call(Request, From, State) ->
    ?debugFmt("cache_worker not have call Request: ~p, From: ~p.", [Request, From]),
    {reply, ok, State}.


do_cast({readahead, false}, State) ->
    {noreply, State};

do_cast({readahead, true}, State) ->
    Module = State#state.module,
    Module:db_load(all),
    {noreply, State};

do_cast({save, Key, Data}, State) ->
    Module = State#state.module,
    ets:insert(Module, {Key, Data}),
    do_update_time(Module, Key, Data, State#state.save_time),
    {noreply, State};

do_cast({delete, Key}, State) ->
    Module = State#state.module,
    case ets:lookup(Module, Key) of
        [Object] ->
            ets:delete_object(Module, Object),
            Module:db_delete(Key);
        _ -> ok
    end,
    {noreply, State};

do_cast(Msg, State) ->
    ?debugFmt("cache_worker not have cast Msg: ~p.", [Msg]),
    {noreply, State}.


do_info(save_time, State) ->
    do_save(State#state.module, State#state.save_time),
    send_after(State#state.save_time, save_time),
    {noreply, State};

do_info(gc_time, State) ->
    send_after(State#state.gc_time, gc_time),
    do_gc(),
    {noreply, State};


do_info(Info, State) ->
    ?debugFmt("cache_worker not have info Info: ~p.", [Info]),
    {noreply, State}.


%%%-------------------------------------------------------------------
do_update_time(Module, _Key, Data, 0) ->
    Module:db_save([Data]);
do_update_time(_Module, Key, Data, SaveTime) ->
    SaveList1 = get_save_list(),
    SaveList2 = lists:keystore(Key, 1, SaveList1, {Key, Data, SaveTime}),
    set_save_list(SaveList2).


do_save(Module, SaveTime) ->
    Timer = erlang:system_time(second) - SaveTime,
    Pred = fun({_K, _D, T}) -> T < Timer end,
    {SaveList, HoldList} = lists:partition(Pred, get_save_list()),
    Module:db_save([D || {_K, D, _T} <- SaveList]),
    set_save_list(HoldList),
    ok.


do_gc()->
    %% 清空缓存
    ok.

%%%-------------------------------------------------------------------
get_save_list() ->
    case erlang:get(save_list) of
        undefined -> [];
        List -> List
    end.
set_save_list(List) ->
    erlang:put(save_list, List).


%%%-------------------------------------------------------------------
send_after(0, _Msg) -> ignore;
send_after(Timer, Msg) ->
    timer:send_after(Timer * 1000, Msg).
